6W - api 보안 2 - 인가, 어드미션 제어

개요

이번에는 저번 글에 이어 api 서버 보안의 다음 단계들을 살펴본다.
구체적으로는 인가와 승인 제어(admission control) 부분이다.

사전 지식

API 서버 보안


저번 글에서 api 서버 보안을 위한 스텝 중 첫 단계인 인증에 대해서 살펴보았다.
다음 과정은 인가와 승인제어이다.
더 자세한 내용은 시큐리티#API 서버 보안에 담겨 있다.

2.인가(Authorization)

두번째 단계는 인가로, 인증된 주체어떤 대상어떤 행동을 할 수 있는지에 대한 것을 규정한다.
인가는 일반적인 REST 속성을 사용하기에, REST 형식을 잘 알고 사용하는 것이 중요하다.

인가에 사용되는 속성

기본적으로 인가를 위해 사용되는 속성은 총 3가지이다.
RBAC를 안다면 상당히 익숙한 개념일 것이다.
인가를 위해서 고려해야 하는 건 누가? 무엇을? 어떻게? 이다.

api 서버에 행해지는 건 결국 HTTP 방식이니 전부 이들도 HTTP 메서드를 가지고는 있기는 하나, 리소스에 대해서는 더 세부적인 메서드를 인가를 위한 동사로 활용하는 것이다.
image.png
이런 식으로 매칭된다.
GET, DELETE 부분을 보면 알겠지만 개별적으로 가져올지, 지켜볼지 등으로 조금 더 세분화한 것으로 보면 된다.

주의!

근데 참고로 어떤 유저가 list 동사가 있고 get 동사가 없다고 쳐도 결국 받아볼 수 있는 정보의 범위는 동일하다!
실제로 들어오는 데이터 상으로는 내부의 모든 데이터를 볼 수 있게 된다.
단순하게 현재 존재하는 시크릿에 list 동작을 해도 데이터를 json으로 뜯어보면 내부 데이터를 다 볼 수 있다는 것.

여기에, 추가적으로 인가와 관련된 특수 동사들이 존재한다.

몇 가지 특수한 것들이 있기는 하나, 전반적으로는 인가에 관련한 속성들은 모두 일반적인 rest api에서 활용되는 값들이다.
그래서 클러스터 인가 설정을 하면서도, 동시에 다른 운영 요소들에 대한 인가 설정을 동시에 적용할 수 있다는 장점이 있다.
가령 하나의 인가 관련 툴을 쓰고 있다면, 이걸로 클러스터 뿐만 아니라 다른 서비스에서도 활용을 할 수 있다는 말이다.

인가 모드

쿠버네티스 인증 모듈과 마찬가지로 인가 역시 여러 개의 모듈을 넣을 수 있다.
또 이것도 순서대로 인가 체크가 이뤄지며, 어느 한 모듈이 허가, 혹은 거부 등의 결정을 내리는 순간 해당 요청은 그대로 적용된다.
다만 다른 점은, 모든 모듈이 결정을 내리지 않은 요청은 무조건 403 에러로 거부된다는 것이다.

참고로 주체가 system:masters 그룹인 경우 이 주체는 인가 검증을 받지 않는다!
이 그룹은 api 서버에 제한 없이 접근할 수 있는 쿠버네티스 내장 그룹으로 설정됐기 때문이다.
그래서 유저 계정을 이 그룹에 넣는 것은 무조건 지양해야 한다.
비슷한 권한을 가지면서 인가 검증을 받을 수 있도록, cluster-admin 클러스터롤을 제공하니 이걸 사용하자.

다음의 모드가 있고, 이들을 여러 개 나열하여 사용할 수 있다.
api 서버에서 --authorization-mode=RBAC,ABAC 이런 식으로 인자로 주면 된다.

RBAC(Role-Based Access Control)

가장 기본적으로 사용되는 방식으로, 유저의 역할에 따라 인가를 결정한다.
쿠버에서는 이를 위한 api를 따로 두고 있어서, 흔히 이게 사용된다.
내용이 길어서 자세한 건 쿠버 RBAC에서 담는다.

유의점이 있는 게, RBAC는 허가만 할 뿐 거부하지 않는다.
어떤 유저는 어떤 것에 어떤 동작을 할 수 있다를 지정하지만 어떤 것을 할 수 없다를 지정하지 않는다는 말이다.
그래서 아래 있는 [[#AlwaysAllow]]와 같이 사용할 경우 실질적으로 모든 요청이 인가되니까 주의가 필요하다.

Webhook

원격 서비스에 동기적인 웹훅을 날려 응답을 받고, 이를 통해 인가를 수행하는 모드이다.[1]
--authorization-webhook-config-file에 웹훅 서버 관련 설정을 넣으면 된다.
웹훅 서버에 대한 설정은 kubeconfig 파일 형식으로 작성해야 한다.

{
  "apiVersion": "authorization.k8s.io/v1beta1",
  "kind": "SubjectAccessReview",
  "spec": {
    "resourceAttributes": {
      "namespace": "kittensandponies",
      "verb": "get",
      "group": "unicorn.example.org",
      "resource": "pods"
    },
    "user": "jane",
    "group": [
      "group1",
      "group2"
    ]
  }
}

쿠버네티스 인증에서는 TokenReview가 날아갔지만 인가에서는 SubjectAccessReview가 날아간다.
그리고 관련한 주체, 대상, 동사에 대한 정보도 함께 날아가도록 돼있다.

{
  "apiVersion": "authorization.k8s.io/v1beta1",
  "kind": "SubjectAccessReview",
  "status": {
    "allowed": false,
	"denied": true,
    "reason": "user does not have read access to the namespace"
  }
}

그럼 웹훅 서버는 이런 식으로 status필드에 응답을 작성하면 된다.
reason을 작성해서 첨부할 수도 있다.
웹훅 모드는 굉장히 중요한 포인트를 하나 가지는데, 그것은 바로 명시적 거부가 가능하다는 것이다!
RBAC도, 아래의 ABAC도 명시적 허용만 가능하고 거부하는 것이 불가능하다.
그러나 웹훅 모드는 denied: true 를 넣어서 보내면 명확하게 인가가 거부된다.
이렇게 되면 이후 다른 모듈에서 추가적으로 인가 검증을 하는 일이 발생하지 않기 때문에 활용도가 높다고 할 수 있겠다.

{
  "apiVersion": "authorization.k8s.io/v1beta1",
  "kind": "SubjectAccessReview",
  "spec": {
    "resourceAttributes": {
      "verb": "list",
      "group": "",
      "resource": "pods",
      "fieldSelector": {
        "requirements": [
          {"key":"spec.nodeName", "operator":"In", "values":["mynode"]}
        ]
      },
      "labelSelector": {
        "requirements": [
          {"key":"example.com/mykey", "operator":"In", "values":["myvalue"]}
        ]
      }
    },
    "user": "jane",
    "group": [
      "group1",
      "group2"
    ]
  }
}

추가적으로 Kubernetes v1.32 - Penelope 기준 베타인 기능으로 라벨이나 필드를 통해 오브젝트를 선별해서 인가 결정을 내릴 수 있다.
이를 통해 훨씬 세밀한 인가 제어가 가능할 것이다.
응답 반환은 똑같은 방식으로 해주면 된다.

왜 RBAC는 셀렉터로 설정 안 해줌 빼액

이라고 생각했는데, 관련한 논의가 이미 진행된 것이 있다.[^5]
이미 다양한 사람들이 이 기능을 바라고 있는데, 왜인지 개발이 진행되지 않고 있다.
이런 걸 안 해주니까 쿠버 RBAC을 사람들이 안 쓰려 하는 거 아니겠나

AlwaysAllow

그냥 모든 요청을 받는 설정이다.
당연히 매우 위험하며, 지양해야 한다.
다른 모듈이랑 같이 쓰면 안전하겠지 하는 생각은 버려야 한다.
왜냐, 위에 [[#RBAC(Role-Based Access Control)]]의 경우 거부를 하지 않기 때문에 허가되지 않은 요청은 그냥 결정을 내리지 않는다.
그러면 이 모드가 동작하게 될 것이고, 결국 무조건 모든 요청이 허용되는 꼴이 되어버린다.

RBAC 심화 - 롤, 바인딩

인가 설정을 할 때 가장 많이 쓰이는 RBAC를 조금 더 깊게 다뤄보고자 한다.

쿠버네티스에서는 RBAC를 위해 4가지 오브젝트를 지원한다.
크게 두 가지 기준으로 4가지로 분화됐다.

처음 이해할 때 팍 와닿는 것은 각각의 쓰임새를 토대로 먼저 Role, Binding 두 유형으로 분류하는 것이다.

이러한 유형 분류 속에, 클러스터 전역적인 설정인지 특정 네임스페이스에 대한 설정인지로 클롤과 롤, 클롤바와 롤바로 나뉜 것이다.

그렇다면 위의 그림처럼 이들을 사용할 때 어떤 식의 동작이 이뤄지는지 간단히 예시를 들어보겠다.

The ClusterRoleBinding "clusterrolebinding-role" is invalid: roleRef.kind: Unsupported value: "Role": supported values: "ClusterRole"

마지막 케이스는 생각을 해봐도 사실 그다지 이치에 맞지 않을 것이다.
클러스터롤바인딩이란 클러스터 전역에서 사용할 수 있는 동작을 매핑시키는 행위인데 이걸 특정 네임스페이스의 롤과 매핑을 시킨다는 것 자체가 이상하다.
그래서 위처럼 에러가 발생하는 것을 확인할 수 있다.

또 하나 유심히 볼 것은 클러스터롤-롤바인딩과 롤-롤바인딩이 결국 유저에게 주어진 권한이 같다는 것이다.
실제로도 그러한데, 전자의 경우는 나중에 같은 동작을 다른 네임스페이스에서도 롤바인딩을 하는데 사용할 수 있다는 점이 다르다.
그래서 네임스페이스 종속적으로 설정할 필요는 있는데, 여러 네임스페이스에 대해서 지정할 때 유용한 방식이다.

이제부터 유형 별로 오브젝트 작성법을 묶어서 살펴보자.
더 자세한 내용은 쿠버 RBAC를 참고하도록 한다.

Roles

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  namespace: default
  name: pod-reader
rules:
- apiGroups: [""]
  resources: ["pods", "pods/log", "services"]
  resourceName: ["name"]
  verbs: ["get", "watch", "list"]
 - apiGroups: ["apps"]
  resources: ["deployments"]
  verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] 
---
kind: ClusterRole
metadata:
  name: monitoring-endpoints
rules:
- apiGroups: [""]
  resources: ["nodes"]
  verbs: ["get"]
- nonResourceURLs: ["/healthz", "/healthz/*"] 
  verbs: ["get", "post"]

롤은 메타데이터에 네임스페이스를 지정해주어야 하고, 반면 클롤은 오히려 네임스페이스가 없어야 한다.

rules

.rules 필드에 허용하고자 하는 동사와 리소스를 리스트로 써주면 된다.
GET /api/v1/namespaces/{namespace}/pods/{name}/log 어떤 리소스를 정의할 때는 이런 api 경로를 참고하면 된다.
즉, apiGroups에는 어떤 api 그룹인지, resources에는 어떤 리소스 유형인지를 적으면 된다.
(헷갈리면 쿠버네티스 API 구조 참고)
위에 pods/log와 같은 식으로 서브 리소스를 명시하는 방식도 가능하다.
개별적으로 작성하기 싫다면 그냥 *로 와일드카드로 사용해도 된다.
리소스 중에서 특정한 놈만 명시하고 싶다면 resourceName을 사용하면 되겠다.

클롤의 경우에는 클러스터 범위의 리소스를 지정하는 것도 가능하다.
대표적인 것이 네임스페이스, 노드 같은 것들이 있다.
추가적으로, 클롤은 리소스가 아닌 비리소스에 대한 동작도 지정할 수 있는데 nonResourceURLs를 이용해 작성하면 된다.
그래서 클롤의 사용례 다음과 같이 정리할 수 있다.

  1. 각 네임스페이스 모두에 적용될만한 권한을 정의할 때
  2. 특정 네임스페이스의 리소스를 모든 네임스페이스에 허용할 때
  3. 클러스터 전역의 리소스를 설정할 때

aggregationRule

클러스터롤의 경우 여러 클러스터롤을 한꺼번에 결합하는 기능을 제공한다.

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: monitoring
aggregationRule:
  clusterRoleSelectors:
  - matchLabels:
      rbac.example.com/aggregate-to-monitoring: "true"
rules: [] # 여기는 채워도 덮어씌워지니 빈 값으로 넣어야 한다.

컨트롤러가 aggregationRule라벨 셀렉터로 매칭된 클롤들을 하나의 클러스터롤로 엮이도록 추적하고 관리해준다.
위의 케이스라면, monitoring이라는 클러스터롤은 저 라벨에 해당하는 클러스터롤의 규칙을 받게 된다.
이 방식의 장점은 클러스터롤을 동적으로 관리할 수 있다는 것이다.

RoleBindings

apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: read-secrets
  # 클러스터롤에 대해 바인딩할 때도 항상 네임스페이스를 신경써야 한다.
  namespace: development
subjects:
- kind: User
  name: dave@test.com
  apiGroup: rbac.authorization.k8s.io
- kind: ServiceAccount
  name: default 
  namespace: kube-system
roleRef:
  kind: ClusterRole
  name: secret-reader
  apiGroup: rbac.authorization.k8s.io
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: read-secrets-global
subjects:
- kind: Group
  name: manager 
  apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: ClusterRole
  name: secret-reader
  apiGroup: rbac.authorization.k8s.io

바인딩을 할 때도 마찬가지로 롤바는 네임스페이스를 신경써서 넣어줘야 한다.
roleRef 필드에 클롤이나 롤을 넣어주면 되는데, 위에서도 말했든 클롤바의 경우 롤을 넣을 수 없단 것을 유의하자.

subjects

이 부분이 어떤 주체가 해당 롤을 받을지를 지정하는 부분이다.
유저와 그룹 이름을 넣을 때는 단순한 문자열로 넣는다고 생각하면 된다.
해당 문자는 이메일 형식도 되고 어떤 형식이든 되는데, 이건 쿠버네티스 인증 단계에서 어떻게 넘겨주냐에 따라 결정될 것이다.
참고로 system: 접두사는 쿠버네티스 시스템에 예약돼있으니, 가급적 일반 유저 앞에 붙이지 않게 인증 단계에서 잘 설정하자.
괜히 복잡성만 증가할 것이다.

서비스 어카운트를 주체로 적을 때는 위와 같이 네임스페이스를 같이 명시해줘야 한다.

subjects:
- kind: Group
  name: system:serviceaccounts:qa
  apiGroup: rbac.authorization.k8s.io

근데 서비스 어카운트를 유저나 그룹으로 명시하는 방법도 있다.
유저로서 명시하고 싶으면 system:serviceaccount:{네임스페이스}:{서비스어카운트} 과 같은 식으로 작성한다.
그룹으로 명시하고 싶다면 system:serviceaccounts:{네임스페이스}처럼 해주면 된다.
그룹일 때 네임스페이스 부분을 빼버리면 그냥 모든 서비스 어카운트에 대해 적용한다는 뜻과 같다.

클러스터에 기본 세팅된 롤과 바인딩

클러스터에 기본적으로 만들어진 클러스터롤과 바인딩 오브젝트가 존재한다.
이것들은 클러스터가 초기 구축됐을 때 설정돼 있으며, 클러스터를 운영할 때 있어서 다양한 요소들을 편하게 관리하기 위해 존재한다.
image.png
대부분은 system:이라는 이름이 접두사로 붙어 쉽게 판별이 가능하다.

labels:
    kubernetes.io/bootstrapping: rbac-defaults

또한 이들은 kubernetes.io/bootstrapping=rbac-defaults 라벨이 붙는다.

이들에는 일단 자동 조정(auto reconcilationi) 기능이 붙어있다.
image.png
이들을 우리가 수정하더라도, api서버가 다시 기동될 때 해당 오브젝트들을 조회한 후 알아서 기본으로 업데이트를 해버린다.

rbac.authorization.kubernetes.io/autoupdate: "true"

구체적으로는 위의 어노테이션을 보고 추적하는 건데, 이런 방식은 두가지 이점이 있다.
일단 관리자 미숙으로 클러스터에 사고가 났을 때, 자동으로 회복될 수 있도록 해준다.
두번째로 클러스터 버전을 올릴 때 알아서 새로운 api그룹이나 버전에 맞게 인가 체크가 될 수 있게 해준다.

rbac.authorization.kubernetes.io/autoupdate: false로 하면 자동 조정이 일어나지 않을 것이다.
그래서 임의로 수정을 하고 싶은 경우에는 꼭 이 어노테이션을 수정해주자!

사용자 중심 롤(user-facing role)

기본으로 세팅된 몇 가지 유형의 롤 중에 하나만 짚어보겠다.
몇몇 기본 클롤은 system:이 접두사로 붙지 않는데, 이것들은 사용자가 사용하도록 의도된 롤이다.
이 클롤들은 위에서 본 [[#aggregationRule]]로 구성돼 있어서, 새로운 api가 추가되더라도 동적으로 접근 권한이 설정된다.

    rbac.authorization.k8s.io/aggregate-to-admin: "true"
    rbac.authorization.k8s.io/aggregate-to-edit: "true"
    rbac.authorization.k8s.io/aggregate-to-view: "true"

수동으로 추가하고 싶다면 직접 만든 클롤에다 이런 라벨을 달아주자.

다음이 유저 중심 클러스터롤이다.

권한 상승 방지(privilege escalation prevention)

RBAC 리소스는 인가 설정을 책임지기에, 권한 없는 유저가 단순히 RBAC 리소스에 대한 권한을 가진다고 해서 함부로 권한을 추가적으로 얻게 되는 것을 막아준다.
이건 api 서버 단에서 막아주는 방식이라 RBAC 모드를 사용하지 않는다고 하더라도 강제 적용된다.

쉽게 이해하기 위해 예를 든다.
a 라는 유저가 있는데, 이 유저는 롤과 롤바에 대한 create 권한만을 가지고 있다.
이 유저는 스스로 파드를 만들 권한을 취득할 수 있을까?
그럴 수 없다가 정답이다.

롤에 대한 권한이 있더라도 다음의 조건이 충족돼야만 주체는 마음대로 다른 리소스에 대한 추가 권한을 획득할 수 있다.

이건 바인딩을 할 때도 마찬가지로, 롤바에 대해 bind 동사 권한을 가지고 있지 않다면 맘대로 바인딩을 만들 수 없다.
그래서 롤과 롤바에 대한 권한을 가지고 있더라도, 주체가 함부로 다른 권한까지 취득할 수 있는 것은 아니다.

정리하자면, 주체는 롤에 대해 escalate 동사, 바인딩에 대해 bind 동사를 가지지 않는 한 자신이 이미 가진 범위 이상의 권한을 획득할 수 없다.

문서에선..

문서에서는 암시적인 방법과 명시적인 방법을 구분한다.
암시적인 방법은 주체에게 주체가 가지고자 하는 관련 권한을 관련한 권한을 가진 다른 관리자가 주는 것을 말한다.
근데 이걸 권한 상승이란 맥락을 설명하기 위한 구절에서 불필요한 설명이라 생각해 생략했다.

그럼, 클러스터 부트스트랩이 일어날 당시에, 각종 기본 세팅된 롤이나 바인딩은 대체 어떻게 만들어질 수 있었던 것인가?!
하는 궁금증이 생긴 사람 아주 칭찬합니다.
이때는 system:masters 그룹의 신원이 사용되는데 위에서도 봤듯이 이 그룹은 RBAC에 대한 어떠한 제약도 받지 않는다..
이 그룹이 cluster-admin 이란 슈우우우우퍼 유저에게 바인딩돼서 각종 작업이 일어나게 되는 것이다.
클러스터를 설치한 초기 관리자가 가지는 권한도 바로 이것으로, 이것은 클러스터가 제대로 구축된 다음에는 사용하지 않는 것이 바람직할 것이다.

3,4. 승인 제어(admission control)

이제 인가 단계 다음, 마지막 승인 제어에 대해서 살펴보자.
승인 제어는 요청의 내용 자체를 변경하거나, 검증해 거부할 수 있는 소프트웨어 모듈이다.
인가 단계에서는 단순히 어떤 대상을 조작할 수 있는에 대한 권한만 다뤘다.
그러나 승인 제어는 세부 대상에 실제로 어떤 요청을 날렸는지 검토하고 조작하는 것이 가능하다.
가령 신뢰된 유저가 들어와서 파드에 대해 create 동작을 허가 받았다.
다만 이 유저가 파드를 만들 때 특정 볼륨을 마운팅하지 않길 바라거나, 제한된 이미지만 이용할 수 있게 만들고 싶다면 이 유저가 보내는 요청 내용을 뜯어서 살펴봐야만 한다.
이럴 때 사용할 수 있는 것이 바로 승인 제어다.

구체적으로 승인 제어는 요청 변형(mutating), 요청 검증(validation) 가지 단계로 나뉘며, 그래서 이게 3,4 단계에 해당한다.

|800
승인 제어는 컨트롤러로서 구현된다.
즉, admission controller라는 놈이 kube-apiserver에 들어있어서 이 놈이 각종 승인 관련 제어를 진행한다.
이 친구는 apiserver가 요청을 받아 수행하여 영구적으로 클러스터에 조작을 가하고 기록을 남기기 직전에 최종적인 단계로서 요청을 인터셉트하여 동작한다.
다양한 승인 제어 플러그인을 설정할 수 있는데, 이들은 전부 순서대로 적용된다.
기본적으로 쿠버네티스에서는 여러 내장 플러그인을 지원한다.
종류가 많기에 플러그인들을 전부 보지는 않겠다.
알아보고 싶다면 직접 문서 참조..

인증 인가 모듈과 다르게, 승인 제어 모듈에서 거부가 한번이라도 일어나면 요청은 바로 거절된다.
주의할 점은 제어가 생성, 변경, 삭제, 혹은 오브젝트 연결 등의 조작을 수행하는 요청에만 동작한다는 것이다.
아무래도 이건 조작을 가하는 요청에 대해 동작하기 위해 존재하다보니, 읽기 관련 요청은 그대로 우회된다.

Mutating 단계

들어온 요청을 조작한다.
가령 기본 네임스페이스에서 파드를 만드는 모든 요청에 대해 초기화 컨테이너를 적용시키고 싶을 때, 이 단계에 적절한 플러그인을 적용해주면 된다.
조건에 걸리는 모든 플러그인들이 해당 요청에 대해 조작을 수행할 것이다.
조작을 가하는 만큼, 각 플러그인이 충돌이 일어나지 않게 하거나 순서를 제대로 지정해주는 것이 관리 포인트 중 하나라 할 수 있다.

Validating 단계

변형이 끝나고 나면 마지막으로 검증을 수행한다.
이 단계에서 요청을 거부할지 결정을 내리게 된다.
기본 네임스페이스에서는 디플로이먼트에 레플리카를 5개 이상 만들지 못하도록 하고 싶고, 이를 벗어난 요청이 일어났을 때는 거부하고 싶다면 이 단계에 플러그인을 적용해준다.

동적 확장

기본적으로 플러그인 적용은 api서버의 재시작을 요구한다.
그러나 플러그인 중에서는 현재 기준 3가지 특별한 플러그인이 있어 기본 세팅된 플러그인 이상으로 관리자가 각종 정책을 동적으로 커스텀할 수 있도록 하는 기능을 제공한다.

이건 Kyverno에서 제공하는 간단한 그림인데, 요지는 각 단계에서 웹훅과 같은 방법으로 동적으로 추가 승인 제어 정책을 넣어줄 수 있다는 것으로 보면 되겠다.

위의 세 가지가 있는데, 앞의 두 개는 이름만 봐도 알 수 있듯이 웹훅을 다른 곳으로 쏴서 승인 제어를 맡기는 방식이다.
세번째는 1.30 버전 기준으로, 승인 제어의 외부 모듈 의존성을 줄이기 위해 쿠버 내장 검증승인정책 방식이다!

Validating Admission Policy

웹훅 방식은 웹훅 서버를 따로 만들어야 한다는, 주체에 따라서는 굉장히 운영 비용이 발생하는 일이 될 수 있다.
이를 위해 쿠버네티스에서는 클러스터 자체적으로 동적으로 승인 제어를 지정할 수 있도록 하는 시도를 하고 있는데, 그 일환 중 하나가 바로 검증 승인 정책이다.[2]
문서에서는 검증 승인 웹훅의 선언적, 내장된 대체제라고 표현한다.
참고로 아직 알파 단계이지만 Mutating Admission Policy도 개발되는 중이다.

이 방식은 CEL 표현식을 통해 검증을 수행할 수 있도록 한다.
그래서 사용하기 위해서는 CEL 표현식을 조금 알아야 하는데, 그렇게 어렵지는 않다.

(위 화살표들은 실제 요청이 검증되기 위해 거치는 흐름을 나타낸 것에 가깝다.) 기본적으로 어떤 리소스가 있고, 어떤 식으로 검증 흐름이 이뤄지는지 담았다. 검증 승인 정책에는 정책(ValidatingAdmissionPolicy)과 정책 바인딩(ValidatingAdmissionPolicyBinding) 두 가지 기본 리소스가 있다. - 정책 - 말 그대로 정책과 대상을 담는 오브젝트 - 바인딩 - 범위와 사용될 추가 리소스를 담는 오브젝트 - 파라미터 - 이건 그냥 **검증 시 파라미터로서 사용할 다른 추가 리소스**를 말한다. - 위처럼 ConfigMap의 값을 변수마냥 쓰는 경우를 말하는데, 사용방식은 아래에서 더 자세히 보자. - 참고로 리소스를 파라미터로 넣을 때, 정책을 만드는 유저는 해당 리소스에 대한 read 권한 정도는 있어야만 한다.

이 문서에서는 파라미터 관련 설명은 빼는데, 관심 있다면 검증 승인 정책 참고.

ValidatingAdmissionPolicy

apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
  name: "demo-policy.example.com"
spec:
  matchConstraints:
    resourceRules:
    - apiGroups:   ["apps"]
      apiVersions: ["v1"]
      operations:  ["CREATE", "UPDATE"]
      resources:   ["deployments"]
  validations:
    - expression: "object.spec.replicas <= 5"
  failurePolicy: Fail

정책의 기본적인 로직이 적히는 오브젝트이다.
예시를 보는 게 이해가 빠른데, 일단 디플로이먼트를 만들거나 업데이트하는 요청에 대해 이 정책이 적용된다.
그리고 검증 규칙은 CEL 표현식으로, 레플리카가 5개 이하여야 참이 된다.
만약 거짓이라면 failurePolicy에 걸리는데, 지금의 경우는 정책에서 실패를 반환한다.

matchConstraints

위처럼 그냥 어떤 리소스를 검증하려는 건지 제약을 지정하는 필드이다.
당연히 와일드카드도 사용할 수 있다.

validations

  validations:
    - expression: "object.spec.replicas <= 5"
      reason: Forbidden
	  message: "params missing but required to bind to this policy"
	  messageExpression: "'object.spec.replicas must be no greater than ' + string(params.maxReplicas)"

검증을 수행하는 필드로, 리스트로 넣으면 차례대로 검증을 수행한다.
하나라도 거짓을 반환하면 일단 거짓이 반환되는데, 이에 대한 결정은 아래 [[#failurePolicy]]에서 결정된다.

reason 필드를 넣어서 거짓이 출력될 값을 지정할 수 있다.
이 값은 그대로 사용자에게는 HTTP 에러 코드로 나타나게 된다.
가능한 값은 다음의 것들이 있는데 세팅하지 않으면 사용자에게는 StatusReasonInvalid라고 뜨게 된다.

CEL 변수

여기에서 CEL을 사용하게 되니, 구체적으로 어떤 변수를 사용할 수 있는 지도 보자.
검증 정책이 적용될 때 사용되는 각 변수는 다음과 같다.

ValidatingAdmissionPolicyBinding

apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
  name: "demo-binding-test.example.com"
spec:
  policyName: "demo-policy.example.com"
  validationActions: [Deny]
  matchResources:
    namespaceSelector:
      matchLabels:
        environment: test

정책을 실제로 적용할 때는 바인딩 오브젝트를 이용한다.
정책 이름을 적고, 검증에 대한 행동을 정의힌다.
그리고 어떤 리소스들이 정책에 해당하게 될지 범위를 지정해준다.
정책에서는 어떤 api 리소스가 적용될지를 지정했다면, 여기에서는 조금 더 클러스터 차원에서 범위를 정하는 느낌이다.

matchResources

어떤 요청이 정책으로 검증될지를 지정하는 필드이다.
namespaceSelector를 사용할 수도 있어서, 조금 더 클러스터 관리적으로 지정할 수 있다는 점이 장점이랄까.

validationActions

정책에서 Fail이라고 결정이 났을 때, 해당 요청을 어떻게 처리할지에 대해 행동을 정의하는 필드이다.

Kyverno

키베르노는 어드미션 컨트롤에서 웹훅을 통해 승인 제어 정책을 마음대로 짤 수 있도록 도와주는 애드온이다.

전체적인 구조는 다음과 같다.

Admission Webhook을 보면 간단하게 어드미션 요청을 웹훅을 쏠 수 있지만, 키베르노는 여기에 각종 다양한 추가 기능이 내장돼있다.
가령 시크릿에 저장된 인증서를 갱신시키거나, 정책에 위반된 요청을 리포트한다던가.

실제로 설치를 하게 된다면, 띄워지는 오브젝트는 다음과 같다.
워크로드로 보자면 4개가 구조적으로 분리돼있다.

Policy


키베르노로 정책을 만들 때는 위와 같은 방식으로 만들면 된다.
하나의 정책을 작성하면 거기에 여러가지 규칙을 지정할 수 있다.
이 규칙은 여러 조건에 따라 요청을 필터링하고, 이후에는 검증하거나, 변형하거나, 생성하는 등의 동작을 수행할 수 있다.

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: require-labels
spec:
  background: false
  rules:
  - name: check-for-labels
    match:
      any:
      - resources:
          kinds:
          - "*"
          operations:
          - CREATE
    validate:
      failureAction: Audit
      message: "The label `app.kubernetes.io/name` is required."
      pattern:
        metadata:
          labels:
            app.kubernetes.io/name: "?*"

이게 예시인데, ClusterPolicy를 만들 수도 있고, 네임스페이스 Policy를 만들 수도 있다.

실습 진행

이번 실습은 굳이 eks가 아니어도 가능하기 때문에 로컬 환경에서 진행했다.
VagrantKubernetes v1.32 - Penelope를 구축해 사용한다.

인가 실습

RBAC 자체는 사실 CKA 시험을 치기 위한 공부를 하거나 기본적으로 쿠버를 공부해봤다면 기본 사용법은 다 알 것이라 생각한다.
그래서 여기에서는 조금 특별한 케이스로 실습을 진행할 것이다.

RBAC 권한 상승시키기

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: sam
rules:
- apiGroups: [""]
  resources: ["pods"]
  verbs: ["get", "watch", "list"]
- apiGroups: ["rbac.authorization.k8s.io"]
  resources: ["roles"]
  verbs: ["get", "create", "delete", "update", "list"]
- apiGroups: ["rbac.authorization.k8s.io"]
  resources: ["rolebindings"]
  verbs: ["get", "create", "delete", "update", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: sam
subjects:
- kind: User
  name: sam
  apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: Role
  name: sam
  apiGroup: rbac.authorization.k8s.io

sam이라는 유저에 대해 Authentication#유저 흉내(impersonation)를 사용하여 실습을 진행할 것이다.
위의 설정에 의해 sam은 파드를 조회만 할 수 있다.
추가적으로 sam은 롤과 롤 바인딩을 만들거나 조회하거나, 삭제할 수 있는 상태이다.
image.png
이렇게 만들어도 system:authenticated 그룹은 자신에 대한 인증과 인가를 확인하는 SelfSubjectReview, SubjectAccessReview 리소스를 사용할 수 있다.
그래서 자신이 누군지 알 수 있다.
image.png
이렇게 권한 체크도 가능하다!
보다시피 보기만 가능하고 만들지는 못 하는 불쌍한 sam...

k access-matrix --as sam -n default

이건 krew 플러그인을 이용해서 설치한 플러그인이다.
image.png
보다시피 sam은 별로 할 수 있는 게 없는 친구다.

이때, sam은 다른 사람들이 파드를 만드는 것을 보고 승이 나서 자신도 마음대로 파드를 만들거나 지우고 싶어졌다!!!
과연 우리 친구 sam이 파드를 만드려면 어떻게 해야 할까?

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: sam-escalate
rules:
- apiGroups: [""]
  resources: ["pods"]
  verbs: ["create", "delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: sam-escalate
subjects:
- kind: User
  name: sam
  apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: Role
  name: sam-escalate
  apiGroup: rbac.authorization.k8s.io

단순하게는, sam이 이렇게 직접 롤과 롤바를 만들면 될 것이다.

kaf sams-escalate.yaml --as sam

image.png
그렇지만 불쌍한 sam은 자신의 권한을 벗어난 롤을 만들 수 없다.
image.png
(관리자 권한으로 이번엔 먼저 sams-escalate 롤을 만든 상태)
그럼 이미 롤이 있다면 그것에 바인딩을 할 수 있냐, 그것도 아니다.
결국 함부로 권한이 상승되는 것이 원천적으로 막혀있는 것이다!

이제 sam을 그만 괴롭히고 파드를 만들 수 있게 해주자.
(미리 만든 롤과 롤바를 적절하게 삭제하는 작업을 해주자.)

- apiGroups: ["rbac.authorization.k8s.io"]
  resources: ["roles"]
  verbs: ["get", "create", "delete", "update", "list", "bind"]

먼저 이미 만들어진 롤에 대해 바인딩을 할 수 있도록 bind 동사를 준다.
이 동사는 롤에 주는 거지 바인딩에 주는 게 아니란 걸 유의하자.
image.png
이번에는 정상적으로 롤바인딩이 이뤄진다.
(롤은 이미 만들어진 상태)
image.png
비로소 sam은 파드를 만들 수 있게 됐다.

- apiGroups: ["rbac.authorization.k8s.io"]
  resources: ["roles"]
  verbs: ["get", "create", "delete", "update", "list", "bind", "escalate"]

이번에는 sam이 아예 롤을 만들 수도 있게 권한을 줘보도록 한다.
image.png
이번에는 롤까지도 sam이 직접 만들 수 있게 된다.

와일드카드 사용 시

- apiGroups: ["rbac.authorization.k8s.io"]
  resources: ["roles"]
  verbs: ["*"]
- apiGroups: ["rbac.authorization.k8s.io"]
  resources: ["rolebindings"]
  verbs: ["*"]

귀찮은 관리자가 대충 와일드 카드로 모든 권한을 주게되면, sam은 마음대로 권한 상승을 할 수 있을까?
image.png
오우.. 가능하다.
권한 상승을 함부로 가능하게 하고 싶지 않았던 관리자는 대충 설정했다가 큰 낭패를 보게 될 수도 있다.

승인 제어 실습

kyverno 활용

현재 굉장히 많이 활용되고 있는 키베르노 애드온은 먼저 이용해보자.

설치

helm repo add kyverno https://kyverno.github.io/kyverno/
helm repo update
helm install kyverno kyverno/kyverno -n kyverno --create-namespace

image.png
간단하게 헬름으로 설치하면 다음과 같은 리소스들이 추가된다.
image.png
이들은 전부 CRD로 추가된 것을 확인할 수 있다.

검증 - pod exec과 hostpath 사용 막기

첫 번째로, default 네임스페이스에서 파드 exec을 하려는 행위를 막아보자.

apiVersion: kyverno.io/v1
kind: Policy
metadata:
  name: deny-pod-exec
spec:
  background: false
  rules:
  - name: pods
    match:
      any:
      - resources:
          kinds:
          - Pod/exec
    validate:
      failureAction: Enforce
      message: "How dare you execute Pod?"
      deny:
        conditions:
          all:
            - key: "{{ request.namespace }}"
              operator: Equals
              value: default

첫번째는 이렇게 작성했다.
일단 모든 파드에 대해서 검사를 하긴 하는데, 그중에서 default 네임스페이스에 대해서는 exec을 막는다.
background는 policyreport와만 관련되는 줄 알았는데, 이걸 false로 해줘야 정상적으로 exec을 막을 수 있는 정책이 생성되더라.
image.png
이런 식의 출력이 나온다.

k exec debug -- whoami

image.png
성공적으로 exec 이 막히는 것이 확인된다.

apiVersion: kyverno.io/v1
kind: Policy
metadata:
  name: deny-hostpath
spec:
  validationFailureAction: Enforce
  background: true
  rules:
    - name: host-path
      match:
        any:
        - resources:
            kinds:
              - Pod
      validate:
        message: >-
          HostPath volumes are forbidden. The field spec.volumes[*].hostPath must be unset.          
        pattern:
          spec:
            =(volumes):
              - X(hostPath): "null"

다음으로는 hostPath를 함부로 생성하는 파드 요청을 막는다.
이번에는 pattern을 이용해서 특정 필드를 직접적으로 들어가 검사를 수행한다.
괄호를 치는 문법은 앵커라 하여, 조건문을 넣는 것과 같은 효과를 낸다.
앞에 등호가 들어가면 해당 값이 있을 경우 하위 필드를 진행하라는 뜻이다.
volumes가 있다면, 하위 필드를 검사하게 된다.
앞에 X가 붙으면 해당 값이 있으면 안 된다는 것이다.
(뒤에 null이 있던 말던 평가되지 않음)
그러니 hostPath라는 필드가 존재하는 순간 검증에서 fail을 반환하는 정책인 것이다.

apiVersion: v1
kind: Pod
metadata:
  name: "hostpath-pod"
spec:
  containers:
  - name: hostpath-pod
    image: "debian-slim:latest"
    volumeMounts:
    - name: localtime
      mountPath: /etc/localtime
  volumes:
    - name: localtime
      hostPath:
        path: /usr/share/zoneinfo/Asia/Taipei

슬쩍 호스트패스를 사용해보려 했으나..
image.png
이런 에러를 내면서 실패한다.

변형 - 워크로드 레플리카 개수 고정시키기

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: mut-replica
spec:
  rules:
  - name: deployment
    match:
      any:
      - resources:
          kinds:
          - Deployment
    mutate:
      patchStrategicMerge:
        spec:
          replicas: 2

변형에서는 디플로이먼트의 레플리카 무조건 2개로 고정시키는 식으로 짰다.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: rep
  labels:
    app: rep
spec:
  selector:
    matchLabels:
      app: rep
  replicas: 10
  template:
    metadata:
      labels:
        app: rep
    spec:
      containers:
      - name: rep
        image: nginx
        imagePullPolicy: IfNotPresent

디플로이먼트에서 레플리카는 10개로 설정했다.
image.png
그러나 막상 만들어지는 레플리카를 보면 무조건 2개로 고정된다.
설정이 제대로 적용된 것이다.
image.png
그러나 이 설정은 scale 서브리소스를 쓰는 것에 대한 제한을 걸진 않기에, k scale을 해서 이후에 레플리카를 늘리는 것은 가능하다.
image.png
한 정책에 대해, 적용됐던 리포트가 각 대상 별로 만들어지는 것이 확인된다.

k describe policyreports.wgpolicyk8s.io

image.png
변형에 대한 policyreport를 확인할 수 있는데, 변형됐다는 사실이 고스란히 남는다.

생성 - 네임스페이스 생성 시 다른 리소스 생성

마지막으로 실습해볼 것은 생성 기능이다.
이것은 어떤 오브젝트가 생성될 때 다른 오브젝트를 같이 만드는 방식으로 동작한다.
이번에는 네임스페이스가 만들어질 때 별도의 서비스어카운트가 만들어지고, 이 서비스 어카운트는 파드 조회가 가능하도록 만들 것이다.

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: kyverno:generate-sa-role-rolebinding
  labels:
    rbac.kyverno.io/aggregate-to-background-controller: "true"  
rules:
- apiGroups: ["rbac.authorization.k8s.io"]
  resources: ["roles", "rolebindings"]
  verbs: ["get", "create", "update", "delete"]
- apiGroups: [""]
  resources: ["serviceaccounts"]
  verbs: ["create", "update", "delete"]

첫번째로 해주어야 할 것은 kyverno background controller 서비스 어카운트가 각종 조작을 수행할 수 있도록 적절한 권한을 주는 것이다.
aggregation 방식을 이용해 쉽게 서비스 어카운트에 권한을 주는 것이 가능하다.
image.png
다행히 생성할 권한이 없다면 알아서 먼저 에러를 뱉어주므로, 이걸 보고 빠르게 세팅을 해주면 된다.

k get clusterrole kyverno:background-controller -oyaml

image.png
덕분에 동적으로 별도의 설정 없이도 편하게 세팅이 가능하다.
image.png
제대로 세팅했는지는 이렇게도 확인 가능하다.

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: gen-sa
spec:
  rules:
  - name: sa
    match:
      any:
      - resources:
          kinds:
          - Namespace
    exclude:
      any:
      - resources:
          namespaces:
          - kube-system
          - default
          - kube-public
          - kyverno
    generate:
      apiVersion: v1
      kind: ServiceAccount
      name: pod-reader
      namespace: "{{request.object.metadata.name}}"
  - name: role
    match:
      any:
      - resources:
          kinds:
          - Namespace
    exclude:
      any:
      - resources:
          namespaces:
          - kube-system
          - default
          - kube-public
          - kyverno
    generate:
      apiVersion: rbac.authorization.k8s.io/v1
      kind: Role
      name: pod-reader
      namespace: "{{request.object.metadata.name}}"
      data:
        rules:
          - apiGroups: [""]
            resources: ["pods"]
            verbs: ["get", "list", "watch"]
  - name: rolebinding
    match:
      any:
      - resources:
          kinds:
          - Namespace
    exclude:
      any:
      - resources:
          namespaces:
          - kube-system
          - default
          - kube-public
          - kyverno
    generate:
      apiVersion: rbac.authorization.k8s.io/v1
      kind: RoleBinding
      name: pod-reader
      namespace: "{{request.object.metadata.name}}"
      data:
        subjects:
          - kind: ServiceAccount
            name: pod-reader
            namespace: "{{request.object.metadata.name}}"
        roleRef:
          apiGroups: rbac.authorization.k8s.io/v1
          kind: Role
          name: pod-reader

다음으로는 실제 정책을 작성한다.
설정 파일이 너무 길어져서 이걸 조금 묶어서 처리할 수 없나 찾아봤는데, 별도로 한번에 여러 개의 generate 규칙을 작성하는 방법은 없는 것 같다.

k create ns test

바로 네임스페이스를 하나 만들어본다.
image.png
성공적으로 내가 원하는 서비스어카운트가 만들어진 것이 보인다.

k auth can-i --as system:serviceaccount:test:pod-reader get po -n test
k -n test get po --as system:serviceaccount:test:pod-reader

image.png
해당 서비스어카운트는 적절하게 인가 권한을 부여받았으므로 자신의 네임스페이스에서 파드를 조회할 수 있다.

Validation Admission Policy 활용

과연 Kyverno에 비해 이후에 개발되어 내장된 VAP는 간편할까?
키베르노에서 진행한 검증 관련 부분들을 똑같이 해보자.

default 네임스페이스 exec 행위 막기

apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
  name: "deny-log-policy"
spec:
  failurePolicy: Fail
  matchConstraints:
    resourceRules:
    - apiGroups:   [""]
      apiVersions: ["v1"]
      # operations:  ["*"]
      operations:  ["CREATE", "UPDATE"]
      resources:   ["pods"]
      scope: Namespaced
  validations:
    - expression: "object.spec.replicas <= 5"

일단 아주 간단하게 정책을 만들었는데, 이전 예제를 따라하면서 타입체크 기능도 확인하고자 일부러 잘못된 표현식을 작성했다.

k describe validatingadmissionpolicy deny-log-policy

image.png
리소스 쪽에서 와일드카드를 사용하지 않으면 확실하게 타입체크 기능이 발동된다.

apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
  name: "deny-exec-policy"
spec:
  failurePolicy: Fail
  matchConstraints:
    resourceRules:
    - apiGroups:   [""]
      apiVersions: ["v1"]
      operations:  ["CONNECT"]
      resources:   ["pods/exec"]
  validations:
    - expression: "false"
      messageExpression: "'subResource :: ' + string(request.subResource) + ' is denied for ' + string(request.requestKind.kind) "

만약 파드에 exec을 하는 경우에는 무조건 거짓을 반환하도록 만들었다.

apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
  name: "default-deny-exec"
spec:
  policyName: "deny-exec-policy"
  validationActions: [Deny]
  matchResources:
    namespaceSelector:
      matchLabels:
        kubernetes.io/metadata.name: default

default 네임스페이스에 대해 적용되도록 설정했다.
image.png

The pods "debug" is invalid: : ValidatingAdmissionPolicy 'deny-exec-policy' with binding 'default-deny-exec' denied request: subResource :: exec is denied for PodExecOptions

성공적으로 실패하는 것이 확인된다.
또한 의도한 대로 메시지도 출력되고 있다.

hostPath 볼륨 만들기 막기

apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
  name: "deny-hostpath-policy"
spec:
  failurePolicy: Fail
  matchConstraints:
    resourceRules:
    - apiGroups:   [""]
      apiVersions: ["v1"]
      operations:  ["CREATE", "UPDATE"]
      resources:   ["pods"]
  validations:
    - expression: "!object.spec.volumes.exists(x, has(x.hostPath))"
      message: "nono hospath nono"
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
  name: "default-deny-hostpath"
spec:
  policyName: "deny-hostpath-policy"
  validationActions: [Deny]
  matchResources:
    namespaceSelector:
      matchLabels:
        kubernetes.io/metadata.name: default

이번에는 간단하게 hostPath 볼륨 마운팅을 막아본다.
exists로 하고 not을 붙인 이유는 all로 할 경우 모든 볼륨을 검사하게 되기 때문이다.
그래서 hostPath 볼륨이 한번이라도 나오는 순간 true를 반환하고, 그것에 not을 붙여서 빠르게 연산되도록 만들었다.

apiVersion: v1
kind: Pod
metadata:
  name: "hostpath-pod"
spec:
  containers:
  - name: hostpath-pod
    image: "debian-slim:latest"
    volumeMounts:
    - name: localtime
      mountPath: /etc/localtime
  volumes:
    - name: localtime
      hostPath:
        path: /usr/share/zoneinfo/Asia/Taipei

사용하는 못된 파드는 이렇게 생겼다.
image.png
노노 호패 노노.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: hostpath2
spec:
  selector:
    matchLabels:
      app: hostpath2
  replicas: 1
  template:
    metadata:
      labels:
        app: hostpath2
    spec:
      containers:
      - name: hostpath2
        image: nginx
        volumeMounts:
        - name: localtime
          mountPath: /etc/localtime
      volumes:
        - name: localtime
          hostPath:
            path: /usr/share/zoneinfo/Asia/Taipei
      restartPolicy: Always

이렇게 막으면 워크로드를 이용할 수 있지 않을까 하는 생각이 들겠지만..
image.png
레플리카셋이 아무리 파드를 만들고 싶어해도 파드 생성 자체가 막히기 때문에 성사되지 못한다.
당연히 이왕이면 워크로드를 만들 시점에 검증 에러를 내뱉는 게 좋을 것이다.

apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
  name: "deny-hostpath-policy"
spec:
  failurePolicy: Fail
  matchConstraints:
    resourceRules:
    - apiGroups:   [""]
      apiVersions: ["v1"]
      operations:  ["CREATE", "UPDATE"]
      resources:   ["pods"]
    - apiGroups:   ["apps"]
      apiVersions: ["v1"]
      operations:  ["CREATE", "UPDATE"]
      resources:   ["deployments"]
  validations:
    - expression: "object.kind == 'Pod' ? !object.spec.volumes.exists(x, has(x.hostPath)) : true"
      messageExpression: "string(object.kind) + ' with hostpath not allowed'"
    - expression: "object.kind == 'Deployment' ? !object.spec.template.spec.volumes.exists(x, has(x.hostPath)) : true"
      messageExpression: "string(object.kind) + ' with hostpath not allowed'"

그래서 이번에는 이렇게 워크로드를 만들 때의 경우도 추가한다.
지금은 deployment만 걸었으나, 아예 와일드카드를 넣어서 다른 워크로드도 막는 것이 가능할 것이다.
삼항 연산자를 넣은 이유는 각각의 케이스를 정확하게 나누어 평가하기 위해서이다.
이상적인 방향은 오히려 따로 정책을 작성하는 것일지도 모르겠다.
image.png
파드와 디플을 한꺼번에 만드려고 시도했고, 두 경우 모두 각각의 조건식에 걸려서 실패를 한 것이 확인된다.

escalate와 bind가 있는 유저가 secret을 건드리지 못하게 막기

쿠버네티스 인가에서 권한 상승을 시킬 수 있는 대표적인 동사 escalate, bind 두 가지를 보았다.
마지막으로는 이것과 관련한 실습을 해보자.

네임스페이스 관리자 sam씨에게 bind와 escalate 동사를 주었다.
자신의 네임스페이스에서 다른 유저에게 권한을 줄 능력이 있어야 하기 때문이다.
그러나.. 역시 sam 자신이 스스로 시크릿에 대한 권한을 얻는 건 용납하지 못하겠다 크아악!

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: sam-default
rules:
- apiGroups: ["rbac.authorization.k8s.io"]
  resources: ["roles", "rolebindings"]
  verbs: ["get", "watch", "list", "create", "update", "delete", "bind", "escalate"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: sam-default
subjects:
- kind: User
  name: sam
  apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: Role
  name: sam-default
  apiGroup: rbac.authorization.k8s.io
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: secret-admin-role
rules:
- apiGroups: [""]
  resources: ["secrets"]
  verbs: ["get", "watch", "list", "create", "delete"]

이렇게 권한을 주었다.
여기에 추가적으로 하나의 롤을 더 만들었는데, 이것은 이미 존재하는 시크릿에 대한 롤이다.
sam은 이 롤을 이용해 자신에게 롤바인딩을 하는 것이 불가능해야 한다.

k access-matrix --as sam -n default 

image.png
일단 sam은 롤에 대한 권한은 적절하게 가지고 있다.

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: sam-secret
rules:
- apiGroups: [""]
  resources: ["secrets"]
  verbs: ["get", "watch", "list", "create", "delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: sam-secret
subjects:
- kind: User
  name: sam
  apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: Role
  name: sam-secret
  apiGroup: rbac.authorization.k8s.io
--- 
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: sam-using-secret-role
  namespace: default
subjects:
- kind: User
  name: sam
  apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: Role
  name: secret-admin-role
  apiGroup: rbac.authorization.k8s.io

이게 sam이 시크릿을 보려고 자신의 권한을 올리는 예시이다.
image.png
bind와 escalate가 있는 한 sam은 쉽게 자신의 권한을 획득할 수 있는 상태이다.
막아야겠지?

apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
  name: "deny-secret-policy"
spec:
  failurePolicy: Fail
  matchConstraints:
    resourceRules:
    - apiGroups:   ["rbac.authorization.k8s.io"]
      apiVersions: ["v1"]
      operations:  ["CREATE", "UPDATE"]
      resources:   ["roles", "rolebindings"]
  variables:
    - name: requester
      expression: "request.userInfo.username"
    - name: blacklist
      expression: "string('sam')"
  matchConditions:
    - name: 'only_subjects'
      expression: "request.userInfo.username == 'sam'"
  validations:
    - expression: "object.kind == 'Role' ? !(object.rules.exists(x, x.resources.exists(y, y == 'secrets'))) : true"
      messageExpression: "string(variables.requester) + ' is not allowed to create ' + string(object.kind) + ' about secrets'"
    - expression: "object.kind == 'RoleBinding' ? !(object.subjects.exists(x, x.name == variables.blacklist) && object.roleRef.name.startsWith('secret')) : true"
      messageExpression: "string(variables.requester) + ' is not allowed to create ' + string(object.kind) + ' about secrets'"
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
  name: "default-deny-secret"
spec:
  policyName: "deny-secret-policy"
  validationActions: [Deny]
  matchResources:
    namespaceSelector:
      matchLabels:
        kubernetes.io/metadata.name: default

정책은 이런 식으로 짰다.
먼저 롤과 롤바인딩을 만드는 요청일 때 이 정책이 적용된다.
추가적으로, 그 요청의 주체가 sam일 경우에만 정책이 적용되도록 만들었다.
조건은 두 가지가 있는데, 먼저 롤에 대해서는 secrets가 리소스에 들어가는 순간 검증 실패 결정을 내린다.
롤 바인딩의 경우엔 바인딩을 하려는 엮으려는 주체가 sam이고 롤의 이름이 secret으로 시작될 때 실패하도록 설정했다.
롤의 이름을 가지고 체크를 하는 이유는 CEL 표현식에서는 현재 들어온 요청의 값에 대해서만 검증을 진행할 수 있기 때문이다.
실제로 롤바인딩 오브젝트가 어떤 권한을 수여하는 롤과 바인딩되려는지, 정책 검증 단계에서는 알 방법이 없다.
그래서 일단 임의로, secret 관련 롤은 이름에 무조건 secret이란 접두사가 들어간다고 가정하고 정책을 짰다.
실무 환경에서도 충분히 이런 네이밍 규칙을 지정하고 사용할 것이라 생각했다.

이러면 secret이 접두사로 들어가지 않은 secret 관련 롤을 만들어서 악용할 수 있지 않을까?
다음의 결과를 보자.

kaf sam-bad-secret.yaml --as sam

image.png
일단 시크릿 관련 롤을 만드는 요청과, 기존에 존재하는 secret 롤을 이용해 롤바인딩을 하려는 요청은 막혔다.
그러나 임의로 만들어진 시크릿 롤에 대한 롤바인딩 요청 자체는 성공한 것이 보인다.

k auth can-i --as sam create secret

image.png
그럼에도 실제로 sam은 시크릿을 만들 수 없는데, 왜냐하면 애초에 sam은 시크릿 관련 롤을 만들 수 없기 때문이다.
이로써 sam은 bind와 escalate 동사를 가지고 있더라도 secret에 대한 권한은 취득할 수 없게 됐다.
sam은 다음의 행동은 할 수 있다.

소기의 목적은 달성됐다!
여기에 롤을 만들 때는 무조건 대상이 되는 리소스를 이름에 접두사로 붙인다던가 하는 검증 규칙을 만들 수도 있을 것이다.
그러나 다 알려주면 재미 없으니 이건 이 글을 보는 분께 심화 과제로 남기고자 한다.

결론

인가

인가 설정을 할 때, 와일드카드를 함부로 넣으면 심각한 보안 문제가 발생할 여지가 많다.
기본적으로 롤과 롤바에 대한 권한을 받을 유저라면 대체로 충분한 권한을 이미 가지고 있을 것이다.
다만 멀티 테넌시 환경에서 네임스페이스 별로 클러스터를 서비스로 제공하거나, 운영 조직의 세분화로 인해 네임스페이스별 관리자를 설정하는 등 적절한 권한 세팅이 필요한 경우가 있을 수 있다.
이 경우 함부로 와일드카드를 쓰는 것은 매우 위험한 일이 될 수 있다.
이를 해소할 수 있는 좋은 방법은 이미 쿠버네티스에서 제공을 하고 있다.
쿠버 RBAC#사용자 중심 롤(user-facing role)에서는 이미 네임스페이스별 관리자를 위한 적절한 권한 부여가 돼있다.
그러므로 네임스페이스 별 운영 권한을 분배해야 하는 경우, 직접 잘 커스텀하여 권한을 설정해주거나, 아니면 최소한 사용자 중심 롤을 이용하자.

승인 제어

kyverno

키베르노의 리포트 오브젝트를 보면서 들었던 생각은, Etcd 용량 부하를 일으킬 것이라는 것이었다.
etcd는 8기가라는 제한이 걸려있는데, 이런 정책 리포트가 계속 추가된다면 이 제한을 자칫 우습게 넘겨버리는 상황이 나올 법하다.
남는 걸까 생각했는데, 그런 식으로 기록을 남겨버리면 순식간에 etcd의 용량이 부족해질 것이다.
쿠버네티스 감사를 별도의 로컬 공간이나 웹훅으로 날리는지만 생각해봐도 당연하게 도출되는 고려사항이다.

이걸 걱정하고 있었는데 보니까 1.12버전부터 리포트 서버를 아예 따로 두는 식으로 기능을 추가한 모양이다.[2:1]
내부적으로는 postgreSQL을 사용하며, 설치도 꽤나 간편하게 되는 것으로 보인다.[3]
방식은 API Aggregation Layer를 통해 기존의 api를 대체하는 방식이다.
내 식견으로는, 이게 부가 세팅을 하는 방식으로 제공될 게 아니라 아예 기본으로 제공해야 한다.
여기에서도 리포트 서버가 없을 때의 문제에 대한 언급이 있다.[4]
24년 5월달에 나온 내용인 걸 보아, 시간이 지나면 자연스레 방식을 바꾸지 않을까 싶다.

검증 승인 정책

내장된 기능이니 최대한 가볍게 설계되지 않았을까 감안하고 실습을 진행했다.
기능은 어느 정도 한정된다고 생각이 들었지만, 검증 정책을 짜는데 있어서는 충분하다고 생각이 든다.

정책을 짜는 것이 어려웠는가?
키베르노와 비교했을 때, 훨씬 쉽다는 생각이 들었다.
CEL 표현식을 익혀야 한다는 게 단점이라면 단점이겠지만, 막상 해보니까 CEL 표현식은 그다지 어렵지 않았다.
image.png
일단 만들어보고 문법 오류가 있다면 이렇게 바로 에러를 내뱉어주기 때문에 생각보다 디버깅도 쉬웠다.
그래서 그냥 CEL 표현식을 공부하는 아주 조금의 수고로, 검증에 대한 것은 편하게 정책을 지정할 수 있다는 게 큰 장점으로 다가왔다.
키베르노까지 깔짝대본 바, 짧은 식견으로는 사용성은 오히려 검증 승인 정책이 더 좋다.
CEL 때문에 더 어려울 것이라 생각했는데, 막상 해보니 절대 그렇지 않다.
물론 키베르노도 검증 승인 정책 방식으로 짤 수 있도록 지원을 해주고 있어서, 이미 사용하고 있는 조직에서는 굳이 이걸 사용하겠답시고 마이그레이션을 할 필요는 절대 없다.
거기에 키베르노는 리포트와 모니터링도 가능하므로 더 다양한 기능을 지원하는 강력한 애드온인 만큼, 이 기능이 내장됐다고 해서 키베르노가 없어질 일은 절대 없다는 것이 내 생각이다.

번외 - 파드 exec과 log는 둘 다 get 요청이지만, 다르다..

처음 검증 정책을 만들 때는 logs에 대해 정책을 걸고 싶었다.
image.png
image.png
보다시피 파드의 서브 리소스인 exec과 log는 api 엔드포인트 방식이 같아서 막연히 log도 막힐 거라 생각했다.
그러나 이후 실습을 하다가 이유를 알게 됐는데, S-exec 명령어가 승인 제어에 걸리는 이유 참고.

이전 글, 다음 글

다른 글 보기

이름 index noteType created
1W - EKS 설치 및 액세스 엔드포인트 변경 실습 1 published 2025-02-03
2W - 테라폼으로 환경 구성 및 VPC 연결 2 published 2025-02-11
2W - EKS VPC CNI 분석 3 published 2025-02-11
2W - ALB Controller, External DNS 4 published 2025-02-15
3W - kubestr과 EBS CSI 드라이버 5 published 2025-02-21
3W - EFS 드라이버, 인스턴스 스토어 활용 6 published 2025-02-22
4W - 번외 AL2023 노드 초기화 커스텀 7 published 2025-02-25
4W - EKS 모니터링과 관측 가능성 8 published 2025-02-28
4W - 프로메테우스 스택을 통한 EKS 모니터링 9 published 2025-02-28
5W - HPA, KEDA를 활용한 파드 오토스케일링 10 published 2025-03-07
5W - Karpenter를 활용한 클러스터 오토스케일링 11 published 2025-03-07
6W - PKI 구조, CSR 리소스를 통한 api 서버 조회 12 published 2025-03-15
6W - api 구조와 보안 1 - 인증 13 published 2025-03-15
6W - api 보안 2 - 인가, 어드미션 제어 14 published 2025-03-16
6W - EKS 파드에서 AWS 리소스 접근 제어 15 published 2025-03-16
6W - EKS api 서버 접근 보안 16 published 2025-03-16
7W - 쿠버네티스의 스케줄링, 커스텀 스케줄러 설정 17 published 2025-03-22
7W - EKS Fargate 18 published 2025-03-22
7W - EKS Automode 19 published 2025-03-22
8W - 아르고 워크플로우 20 published 2025-03-30
8W - 아르고 롤아웃 21 published 2025-03-30
8W - 아르고 CD 22 published 2025-03-30
8W - CICD 23 published 2025-03-30
9W - EKS 업그레이드 24 published 2025-04-02
10W - Vault를 활용한 CICD 보안 25 published 2025-04-16
11W - EKS에서 FSx, Inferentia 활용하기 26 published 2025-04-18
11주차 - EKS에서 FSx, Inferentia 활용하기 26 published 2025-05-11
12W - VPC Lattice 기반 gateway api 27 published 2025-04-27

관련 문서

이름 noteType created
API 접근 제어 우회 knowledge 2025-01-13
Authentication knowledge 2025-01-13
Authorization knowledge 2025-01-19
Admission Control knowledge 2025-01-20
Admission Webhook knowledge 2025-01-20
Prometheus-Adapter knowledge 2025-03-04
kube-apiserver knowledge 2025-03-12
Validation Admission Policy knowledge 2025-03-17
Kyverno knowledge 2025-03-17
쿠버네티스 API 구조 knowledge 2025-03-19
E-api 서버 감사 topic/explain 2025-01-21
E-Kyverno 기본 실습 topic/explain 2025-03-17
E-검증 승인 정책 실습 topic/explain 2025-03-17
S-exec 명령어가 승인 제어에 걸리는 이유 topic/shooting 2025-03-17

참고


  1. https://kyverno.io/policies/other/get-debug-information/get-debug-information/ ↩︎

  2. https://kyverno.io/blog/2024/04/26/kyverno-1.12-released/#reports-server---alternative-reports-storage ↩︎ ↩︎

  3. https://github.com/kyverno/reports-server/blob/main/docs/INSTALL.md ↩︎

  4. https://kyverno.io/blog/2024/05/29/kyverno-reports-server-the-ultimate-solution-to-scale-reporting/ ↩︎